-------------Bubble Bobble-------------
A 4am crack                  2017-08-23
---------------------------------------

Name: Bubble Bobble
Genre: arcade
Year: 1988
Credits: Chris Eisnaugle, NovaLogic
Publisher: Taito America
Media: double-sided 5.25-inch floppy
OS: custom

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  no errors on either side, but copy
  hangs with the drive motor off

Locksmith Fast Disk Backup
  ditto

EDD 4 bit copy (no sync, no count)
  ditto

Copy ][+ nibble editor
  nothing suspicious

Disk Fixer
  T00 is the DOS 3.3 bootloader/RWTS,
    but T00,S01 code is custom and
    there's no sign of the rest of DOS
    on tracks 1 and 2
  no disk catalog on track $11 or any
    other track

Why didn't any of my copies work?
  probably a protection check in early
  boot

Next steps:

  1. Trace the boot
  2. Disable the protection check
  3. Declare victory (*)

(*) go to the gym

                   ~

               Chapter 1
         Boot Trace and Chill


Since my non-working copy fails pretty
quickly, I suspect I can just trace the
boot and find the copy protection check
fairly quickly.

[S6,D1=original disk (side A)]
[S5,D1=my work disk]

]CALL -151

; copy slot 6 drive firmware to lower
; memory so we can patch it
*9600<C600.C6FFM

; break to the monitor after the
; initial sector loop
96F8-   A9 4C       LDA   #$4C
96FA-   8D 4A 08    STA   $084A
96FD-   A9 59       LDA   #$59
96FF-   8D 4B 08    STA   $084B
9702-   A9 FF       LDA   #$FF
9704-   8D 4C 08    STA   $084C
9707-   4C 01 08    JMP   $0801

*9600G
...reboots slot 6...
<beep>

*B700L

B700-   A9 00       LDA   #$00
B702-   85 F1       STA   $F1
B704-   A9 14       LDA   #$14
B706-   85 F2       STA   $F2
B708-   A9 00       LDA   #$00
B70A-   85 F0       STA   $F0
B70C-   A5 F0       LDA   $F0
B70E-   8D 05 0A    STA   $0A05
B711-   A5 F2       LDA   $F2
B713-   8D 04 0A    STA   $0A04
B716-   A9 00       LDA   #$00
B718-   8D 08 0A    STA   $0A08
B71B-   A9 98       LDA   #$98
B71D-   18          CLC
B71E-   65 F1       ADC   $F1
B720-   8D 09 0A    STA   $0A09

; Not shown, but this constructs the
; rest of an RWTS parameter table at
; $0A00 then calls the standard $B7B5
; entry point to read a single sector.
; So we're reading T14,S00 into $9800.
B723-   20 3E B7    JSR   $B73E

; read 6 sectors
B726-   E6 F1       INC   $F1
B728-   A5 F1       LDA   $F1
B72A-   C9 06       CMP   #$06
B72C-   F0 0D       BEQ   $B73B

; into consecutive memory ($9900, &c.)
B72E-   E6 F0       INC   $F0
B730-   A5 F0       LDA   $F0
B732-   C9 10       CMP   #$10
B734-   D0 D6       BNE   $B70C
B736-   E6 F2       INC   $F2
B738-   4C 08 B7    JMP   $B708

; jump to the code we just read
B73B-   4C 00 98    JMP   $9800

Let's capture it.

*C500G
...
*9600<C600.C6FFM

; set callback after loading RWTS and
; boot code
96F8-   A9 4C       LDA   #$4C
96FA-   8D 4A 08    STA   $084A
96FD-   A9 0A       LDA   #$0A
96FF-   8D 4B 08    STA   $084B
9702-   A9 97       LDA   #$97
9704-   8D 4C 08    STA   $084C

; start the boot
9707-   4C 01 08    JMP   $0801

; callback is here --
; jump to monitor after reading these
; sectors (instead of jumping to $9800)
970A-   A9 59       LDA   #$59
970C-   8D 3C B7    STA   $B73C
970F-   A9 FF       LDA   #$FF
9711-   8D 3D B7    STA   $B73D

; continue the boot
9714-   4C 00 B7    JMP   $B700

*BSAVE TRACE,A$9600,L$117
*9600G
...reboots slot 6...
...read read read...
<beep>

*9800L

9800-   20 C5 98    JSR   $98C5

*98C5L

; slow IIgs to 1 MHz
98C5-   AD 36 C0    LDA   $C036
98C8-   29 7F       AND   #$7F
98CA-   8D 36 C0    STA   $C036

; will explore this in a moment
98CD-   A9 00       LDA   #$00
98CF-   8D 4C 99    STA   $994C
98D2-   20 EE 98    JSR   $98EE

; check a flag
98D5-   AD 4D 99    LDA   $994D
98D8-   F0 03       BEQ   $98DD

; is this failure or success?
98DA-   4C 1A 9C    JMP   $9C1A

; since we initially did a JSR to get
; here, I'm guessing "exit gracefully"
; is the success path, but we'll see
98DD-   60          RTS

My intuition tells me that $9C1A is not
a good place to end up.

*9C1AL

; text mode
9C1A-   AD 51 C0    LDA   $C051
9C1D-   AD 4C 99    LDA   $994C
9C20-   4A          LSR
9C21-   4A          LSR
9C22-   4A          LSR
9C23-   4A          LSR
9C24-   AA          TAX
9C25-   BD DE 98    LDA   $98DE,X

; put a character in the upper left
; corner of the screen
9C28-   8D 00 04    STA   $0400
9C2B-   AD 4C 99    LDA   $994C
9C2E-   29 0F       AND   #$0F
9C30-   AA          TAX
9C31-   BD DE 98    LDA   $98DE,X

; and another
9C34-   8D 01 04    STA   $0401

; and a space
9C37-   A9 A0       LDA   #$A0
9C39-   8D 02 04    STA   $0402

; now wipe all of main memory up to
; this routine
9C3C-   A9 3B       LDA   #$3B
9C3E-   A2 00       LDX   #$00
9C40-   86 50       STX   $50
9C42-   A2 02       LDX   #$02
9C44-   86 51       STX   $51
9C46-   A0 00       LDY   #$00
9C48-   A9 A0       LDA   #$A0
9C4A-   91 50       STA   ($50),Y
9C4C-   E6 50       INC   $50
9C4E-   D0 02       BNE   $9C52
9C50-   E6 51       INC   $51
9C52-   A6 51       LDX   $51
9C54-   E0 9C       CPX   #$9C
9C56-   90 F0       BCC   $9C48

; this will end up being an infinite
; loop
9C58-   C9 13       CMP   #$13
9C5A-   F0 02       BEQ   $9C5E
9C5C-   D0 FA       BNE   $9C58
9C5E-   C5 52       CMP   $52
9C60-   D0 F6       BNE   $9C58

I suspect this is where my non-working
copy ends up. Let's see what it takes
not to end up there.

                   ~

               Chapter 2
   Self-Modifying Is Best Modifying


Whatever is setting the flag at $994D,
it's happening inside the routine at
$98EE.

*98EEL

; seek to track $21 (not shown)
98EE-   A9 21       LDA   #$21
98F0-   8D 04 0A    STA   $0A04
98F3-   20 86 98    JSR   $9886

; clear a few bytes of memory,
; including the one that the caller
; checks ($994D)
98F6-   A9 00       LDA   #$00
98F8-   8D 4E 99    STA   $994E
98FB-   8D 4D 99    STA   $994D
98FE-   A0 0F       LDY   #$0F
9900-   99 4F 99    STA   $994F,Y
9903-   88          DEY
9904-   10 FA       BPL   $9900
9906-   20 0A 99    JSR   $990A
9909-   60          RTS

*990AL

990A-   20 60 99    JSR   $9960

*9960L

; clear another $200 bytes of memory
9960-   A9 00       LDA   #$00
9962-   A0 00       LDY   #$00
9964-   99 1A 9A    STA   $9A1A,Y
9967-   99 1A 9B    STA   $9B1A,Y
996A-   C8          INY
996B-   D0 F7       BNE   $9964
996D-   60          RTS

Continuing from $990D...

; increment the all-important flag! ah!
990D-   EE 4D 99    INC   $994D

; don't know what these are
9910-   A9 19       LDA   #$19
9912-   8D D0 99    STA   $99D0
9915-   8D DE 99    STA   $99DE

9918-   20 6E 99    JSR   $996E

*996EL

; reset data latch (slot is hard-coded
; to slot 6, even though the underlying
; OS would boot from any slot -- very
; common in copy protection code)
996E-   A2 60       LDX   #$60
9970-   DD 8E C0    CMP   $C08E,X

; Death Counters?
9973-   A9 C0       LDA   #$C0
9975-   8D F4 99    STA   $99F4
9978-   8D F5 99    STA   $99F5
997B-   EE F4 99    INC   $99F4
997E-   D0 08       BNE   $9988
9980-   EE F5 99    INC   $99F5
9983-   D0 03       BNE   $9988

; failure path?
9985-   4C EC 99    JMP   $99EC

*99ECL

; clear those $200 bytes of memory
; again
99EC-   20 60 99    JSR   $9960

; turn off drive motor (not shown)
99EF-   20 FF 99    JSR   $99FF

; set carry and exit
99F2-   38          SEC
99F3-   60          RTS

OK, if this subroutine fails, it sets
the carry bit and exits. This is the
same convention as DOS uses. But I
still haven't found any "real" code yet
that could constitute a copy protection
check.

Continuing from $9988...

; match "D5 AA 96" (address prologue)
9988-   BD 8C C0    LDA   $C08C,X
998B-   10 FB       BPL   $9988
998D-   C9 D5       CMP   #$D5
998F-   D0 EA       BNE   $997B
9991-   BD 8C C0    LDA   $C08C,X
9994-   10 FB       BPL   $9991
9996-   C9 AA       CMP   #$AA
9998-   D0 E1       BNE   $997B
999A-   BD 8C C0    LDA   $C08C,X
999D-   10 FB       BPL   $999A
999F-   C9 96       CMP   #$96
99A1-   D0 D8       BNE   $997B

; fetch and decode the next address
; field value (this is the disk volume)
99A3-   20 05 9A    JSR   $9A05

; and again (this is the track)
99A6-   20 05 9A    JSR   $9A05
99A9-   C9 21       CMP   #$21
99AB-   8D 4C 99    STA   $994C
99AE-   D0 CB       BNE   $997B

; and again (this is the sector)
99B0-   20 05 9A    JSR   $9A05

; sector = #$0F
99B3-   C9 0F       CMP   #$0F

; otherwise loop back
99B5-   D0 C4       BNE   $997B

Now we're positioned after the address
field of T21,S0F.

; match "D5 AA"
99B7-   BD 8C C0    LDA   $C08C,X
99BA-   10 FB       BPL   $99B7
99BC-   C9 D5       CMP   #$D5
99BE-   D0 F7       BNE   $99B7
99C0-   BD 8C C0    LDA   $C08C,X
99C3-   10 FB       BPL   $99C0
99C5-   C9 AA       CMP   #$AA
99C7-   D0 F3       BNE   $99BC

; read a set of $100 nibbles
99C9-   A0 00       LDY   #$00
99CB-   BD 8C C0    LDA   $C08C,X
99CE-   10 FB       BPL   $99CB
99D0-   19 1A 9A    ORA   $9A1A,Y
99D3-   99 1A 9A    STA   $9A1A,Y
99D6-   C8          INY
99D7-   D0 F2       BNE   $99CB

; read another $100 nibbles, "OR" them
; with the first set
99D9-   BD 8C C0    LDA   $C08C,X
99DC-   10 FB       BPL   $99D9
99DE-   19 1A 9B    ORA   $9B1A,Y
99E1-   99 1A 9B    STA   $9B1A,Y
99E4-   C8          INY
99E5-   D0 F2       BNE   $99D9

; turn off drive, clear carry, and exit
99E7-   20 FF 99    JSR   $99FF
99EA-   18          CLC
99EB-   60          RTS

Oh, hey, wait, hold up, OK, um... let's
revisit a little bit of code in the
caller that I didn't understand at the
time.

``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,
``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,
``                                   .,
``  9910-   A9 19       LDA   #$19   .,
``  9912-   8D D0 99    STA   $99D0  .,
``  9915-   8D DE 99    STA   $99DE  .,
``                                   .,
``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,
``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,

Those memory locations, $99D0 & $99DE,
and in the middle of this subroutine.
#$19 is the 6502 opcode for "ORA", and
those locations were the "ORA"
instructions in the loops that read raw
nibbles from the disk and stored them
at $9A1A and $9B1A.

I mean, I still don't know what's going
on, but that's interesting, right?

                   ~

               Chapter 3
   The Impossible Just Takes Longer


Popping the stack and continuing from
$991B...

; if that subroutine failed (because we
; couldn't find the right sector or
; whatever), give up now
991B-   B0 2D       BCS   $994A

; check a single nibble in the middle
; of the range of raw nibbles we read
991D-   AD 75 9B    LDA   $9B75
9920-   C9 99       CMP   #$99

; if it's not a known value, give up
9922-   D0 26       BNE   $994A

; self-modifying code alert! All of
; those "ORA" opcodes are now "EOR"
9924-   A9 59       LDA   #$59
9926-   8D D0 99    STA   $99D0
9929-   8D DE 99    STA   $99DE

; call the same routine again, but now
; with "EOR"
992C-   20 6E 99    JSR   $996E

For illustration purposes, I'm going to
relist the subroutine as it now stands,
with two (apparently important) opcodes
replaced.

*99D0:59
*99DE:59

*99CBL

; read $100 raw nibbles (the same $100
; as before -- the subroutine has
; already repositioned itself at the
; same point in T21,S0F)
99CB-   BD 8C C0    LDA   $C08C,X
99CE-   10 FB       BPL   $99CB

; but now XOR them with the values we
; got before ?!?!?
99D0-   59 1A 9A    EOR   $9A1A,Y
99D3-   99 1A 9A    STA   $9A1A,Y
99D6-   C8          INY
99D7-   D0 F2       BNE   $99CB

; read the second set of $100 nibbles
99D9-   BD 8C C0    LDA   $C08C,X
99DC-   10 FB       BPL   $99D9

; again, XOR them with the values we
; got before
99DE-   59 1A 9B    EOR   $9B1A,Y
99E1-   99 1A 9B    STA   $9B1A,Y
99E4-   C8          INY
99E5-   D0 F2       BNE   $99D9

This makes no sense at all. We're going
out of our way to read a set of nibbles
from disk, twice, them XOR'ing the ones
we got the second time with the ones
we got the first time.

But nibbles don't change. Any value XOR
itself is always zero. So, like, WTF?

Continuing from $992F, after calling
this routine with the altered opcodes
and -- as far as I can tell -- zeroing
out $200 bytes of memory in the most
roundabout way possible.

992F-   A2 00       LDX   #$00
9931-   A0 0F       LDY   #$0F

; look at a subset of the XOR'd-with-
; itself buffer (interesting -- we're
; looking just after the location we
; checked earlier, at $9919)
9933-   B9 75 9B    LDA   $9B75,Y

; if it's zero, skip next instruction
9936-   F0 01       BEQ   $9939

; if it's not zero (impossible),
; increment X
9938-   E8          INX
9939-   88          DEY
993A-   10 F7       BPL   $9933

; increment one of the bytes we cleared
; way back in the beginning
993C-   FE 4F 99    INC   $994F,X

; if X < 3, skip the next three lines
993F-   E0 03       CPX   #$03
9941-   90 07       BCC   $994A

; decrement $994D (back to 0)
9943-   CE 4D 99    DEC   $994D

; increment a different flag
9946-   EE 4E 99    INC   $994E

; exit to caller either way
9949-   60          RTS
994A-   60          RTS

The success path in the caller verifies
that $994D is 0. It's incremented from
0 to 1 ($990D), then decremented back
to 0 (at $9943), but only if X >= 3.
The only way X is >= 3 is if we read
512 nibbles from disk, twice, and 3 or
more of them change the second time we
read them.

This is the key point: the data being
read from track $21 is non-repeatable.
It's different every time it's read.
How is that possible?

The value at $9B75 (must be #$99) looks
important, but it's not. The important
part is what comes after it: nothing.
Because that's what is actually on the
original disk: nothing.

When we say a "zero bit," we really
mean "the lack of a magnetic state
change." If the Disk II doesn't see a
state change in a certain period of
time, it calls that a "0". If it does
see a change, it calls that a "1". But
the drive can only tolerate a lack of
state changes for so long -- about as
long as it takes for two bits to go by.

Fun fact(*): this is why you need to
use nibbles as an intermediate on-disk
format in the first place. No valid
nibble contains more than two zero bits
consecutively, when written from most-
significant to least-significant bit.

(*) not guaranteed, actual fun may vary

So what happens when a drive doesn't
see a state change after the equivalent
of two consecutive zero bits? The drive
thinks the disk is weak, and it starts
increasing the amplification to try to
compensate, looking for a valid signal.
But there is no signal. There is no
data. There is just a yawning abyss of
nothingness. Eventually, the drive gets
desperate and amplifies so much that it
starts returning random bits based on
ambient noise from the disk motor and
the magnetism of the Earth.

Seriously.

Returning random bits doesn't sound
very useful for a storage medium, but
it's exactly what the developer wanted,
and that's exactly what this code is
checking for. It's finding and reading
the same sequence of nibbles from the
disk, twice, and counting how many of
the nibbles changed.

Bit copiers will never duplicate the
long sequence of zero bits, because
that's not what they read. Whatever
randomness they get when they read the
original disk will essentially get
"frozen" onto the copy. Those nibbles
at $9B75 will always be the same, the
first time and second time around, and
the XOR of any number with itself is
always zero.

God, I hate physical objects.

                   ~

               Chapter 4
       One Byte To Rule Them All


Popping the stack all the way back to
$98D2, we see this code that calls the
copy protection routine and checks a
flag afterwards:

98D2-   20 EE 98    JSR   $98EE
98D5-   AD 4D 99    LDA   $994D
98D8-   F0 03       BEQ   $98DD
98DA-   4C 1A 9C    JMP   $9C1A
98DD-   60          RTS

There is no attempt to obfuscate this
code; it's stored on disk exactly as I
found it. There are no anti-tamper
checks. Despite multiple flags being
set and reset, in the end only one of
them matters: $994D.

Also, one of the first things the real
game code does it overwrite all this
memory with sprite data, so there's no
lingering side effects or delayed
protection checks. It's self-contained,
unintegrated with the rest of the game.
I can change the "JSR $98EE" to "RTS"
and bypass the entire thing.

T14,S00,$D2: 20 -> 60

Quod erat liberandum.

---------------------------------------
A 4am crack                    No. 1381
------------------EOF------------------
